package com.panopset;

import java.io.File;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.panopset.compat.RezIO;
import com.panopset.compat.Strings;
import com.panopset.io.LockedWriter;

/**
 * Provides list of maven dependencies from any pom, provided that the project
 * has been built. Traverses the .m2 repository for dependency hierarchy.
 *
 * @author Karl Dinwiddie
 *
 */
public final class MavenAuditor {

    /**
     * Relocation xpath.
     */
    private static final String XPATH_RELO

    = "/project/distributionManagement/relocation/groupId";

    /**
     * Usage prompt.
     */
    private static final String USAGE

    = "java MavenAuditor <pom.xml path> <report path>";

    /**
     * @param args
     *            args[1] is the path to a pom you would like a report on.
     *            args[2] is the path to the report.
     * @throws Exception
     *             Exception.
     */
    public static void main(final String... args) throws Exception {
        if (args == null || args.length != 2) {
            System.out.println(USAGE);
            return;
        }
        File pomFile = new File(args[0]);
        if (!pomFile.exists()) {
            System.out.println(RezIO.getCanonicalPath(pomFile)
                    + " does not exist.");
            return;
        }
        LockedWriter lw = new LockedWriter(args[1]);
        lw.writeln("file,group,artifact,version,scope");
        for (MavenDependency md : new MavenAuditor()
                .loadPomDependencies(pomFile)) {
            lw.writeln(md.toString());
        }
        lw.close();
    }

    /**
     * Loaded poms.
     */
    private List<String> loadedPoms = new ArrayList<String>();

    /**
     * @param pomFile
     *            Any valid pom.xml file.
     * @return Sorted set of MavenDependency objects.
     * @throws Exception
     *             Exception.
     */
    public Set<MavenDependency> loadPomDependencies(final File pomFile)
            throws Exception {
        final Set<MavenDependency> rtn = Collections
                .synchronizedSortedSet(new TreeSet<MavenDependency>());
        if (!pomFile.exists()) {
            return rtn;
        }
        SAXParserFactory.newInstance().newSAXParser()
                .parse(pomFile, new StandardHelper(rtn, pomFile));
        List<String> dependentPoms = new ArrayList<String>();
        for (MavenDependency md : rtn) {
            if (md.isOptional()) {
                continue;
            }
            File repositoryPom = new File(md.getLocalRepositoryPom());
            if (repositoryPom.exists() && repositoryPom.isFile()) {
                try {
                    if (UtilIO
                            .getFileText(new File(md.getLocalRepositoryPom()))
                            .indexOf("<relocation>") > -1) {
                        md.setGroupId(new PomParser(repositoryPom)
                                .getXPath(XPATH_RELO));
                    }
                } catch (Exception e) {
                    Util.log(e);
                }
            }
            String fn = md.getLocalRepositoryPom();
            if (!loadedPoms.contains(fn)) {
                loadedPoms.add(fn);
                dependentPoms.add(fn);
            }
        }
        List<MavenDependency> replacements = new ArrayList<MavenDependency>();
        for (String fn : dependentPoms) {
            for (MavenDependency rmd : loadPomDependencies(new File(fn))) {
                replacements.add(rmd);
            }
        }
        for (MavenDependency repl : replacements) {
            addDependency(rtn, repl);
        }
        List<MavenDependency> dlts = new ArrayList<MavenDependency>();
        // If no version was ever specified in the pom, we go to the repository
        // and find the latest version.
        for (MavenDependency dep : rtn) {
            if (!Strings.isPopulated(dep.getVersion())) {
                StringWriter sw = new StringWriter();
                sw.append(getMavenHome());
                sw.append(Strings.FILE_SEP);
                sw.append(dep.getPathFromRepositoryRoot());
                sw.append(Strings.FILE_SEP);
                sw.append(dep.getArtifactId());
                String greatestVersion = "";
                File dir = new File(sw.toString());
                if (dir.exists()) {
                    for (File f : dir.listFiles()) {
                        if (Strings.isNewerDotVersion(

                        f.getName(), greatestVersion)) {
                            greatestVersion = f.getName();
                        }
                    }
                }
                dep.setVersion(greatestVersion);
                if (!new File(dep.getLocalRepositoryJar()).exists()) {
                    dlts.add(dep);
                }
            }
        }
        // Remove any exclusions found.
        for (MavenDependency dep : rtn) {
            for (MavenDependency exc : excs) {
                if (exc.getGroupId().equals(dep.getGroupId())
                        && exc.getArtifactId().equals(dep.getArtifactId())) {
                    dlts.add(dep);
                }
            }
        }
        for (MavenDependency dep : dlts) {
            rtn.remove(dep);
        }
        return rtn;
    }

    /**
     * Add a dependency if it is not in a <b>test</b> or <b>system</b> scope.
     *
     * @param rtn
     *            MavenDependency set.
     * @param md
     *            MavenDependency.
     */
    private void addDependency(final Set<MavenDependency> rtn,
            final MavenDependency md) {

        if ("test".equals(md.getScope()) || "system".equals(md.getScope())) {
            return;
        }

        for (MavenDependency emd : rtn) {
            if (emd.getGroupId().equals(md.getGroupId())
                    && emd.getArtifactId().equals(md.getArtifactId())
                    && Strings.isNewerDotVersion(emd.getVersion(),
                            md.getVersion())) {
                return;
            }
        }
        rtn.remove(md);
        rtn.add(md);
    }

    /**
     * Standard helper.
     */
    class StandardHelper extends DefaultHandler {

        /**
         * MavenDependency set.
         */
        private final Set<MavenDependency> rtn;

        /**
         * Pom.xml.
         */
        private final File file;

        /**
         * @param mavenDependencies
         *            MavenDependency set.
         * @param pomFile
         *            Pom.xml.
         */
        StandardHelper(final Set<MavenDependency> mavenDependencies,
                final File pomFile) {
            rtn = mavenDependencies;
            file = pomFile;
        }

        /**
         * Parent project flag.
         */
        private boolean parent = false;

        /**
         * String buffer.
         */
        private StringBuilder sb = new StringBuilder();

        @Override
        public void characters(final char[] buffer, final int start,
                final int length) {
            sb.append(buffer, start, length);
        }

        /**
         * Maven dependency.
         */
        private MavenDependency dep;

        /**
         * Maven dependency.
         */
        private MavenDependency exc;

        /**
         * Project to be included as well.
         */
        private MavenDependency project;

        @Override
        public void startElement(final String uri, final String localName,
                final String qName, final Attributes attributes) {
            sb = new StringBuilder();
            if ("dependency".equals(qName)) {
                dep = new MavenDependency(file);
            } else if ("parent".equals(qName)) {
                dep = null;
                parent = true;
            } else if ("project".equals(qName)) {
                project = new MavenDependency(file);
            } else if ("exclusion".equals(qName)) {
                exc = new MavenDependency(file);
            }
        }

        @Override
        public void endElement(final String uri, final String localName,
                final String qName) throws SAXException {
            if (qName == null) {
                return;
            }
            if ("parent".equals(qName)) {
                parent = false;
                return;
            }
            if (parent) {
                if ("groupId".equals(qName)) {
                    if (exc == null) {
                        if (dep == null) {
                            if (project.depGroupId == null) {
                                project.depGroupId = sb.toString();
                            }
                        } else {
                            dep.setGroupId(sb.toString());
                        }
                    } else {
                        exc.setGroupId(sb.toString());
                    }
                }
                return;
            }
            if ("project".equals(qName)) {
                for (MavenDependency md : rtn) {
                    md.setParent(project);
                }
                addDependency(rtn, project);
            } else if ("dependency".equals(qName)) {
                addDependency(rtn, dep);
                dep = null;
            } else if ("scope".equals(qName)) {
                if (exc == null) {
                    if (dep != null) {
                        dep.depScope = sb.toString();
                    }
                } else {
                    exc.setScope(sb.toString());
                }
            } else if ("groupId".equals(qName)) {
                if (exc == null) {
                    if (dep == null) {
                        if (project.depGroupId == null) {
                            project.depGroupId = sb.toString();
                        }
                    } else {
                        dep.setGroupId(sb.toString());
                    }
                } else {
                    exc.setGroupId(sb.toString());
                }
            } else if ("artifactId".equals(qName)) {
                if (exc == null) {
                    if (dep == null) {
                        if (project.depArtifactId == null) {
                            project.depArtifactId = sb.toString();
                        }
                    } else {
                        dep.setArtifactId(sb.toString());
                    }
                } else {
                    exc.setArtifactId(sb.toString());
                }
            } else if ("version".equals(qName)) {
                if (exc == null) {
                    if (dep == null) {
                        if (project.depVersion == null) {
                            project.depVersion = sb.toString();
                        }
                    } else {
                        dep.setVersion(sb.toString());
                    }
                } else {
                    exc.setVersion(sb.toString());
                }
            } else if ("optional".equals(qName)) {
                if (exc == null) {
                    dep.optional = sb.toString();
                } else {
                    exc.optional = sb.toString();
                }
            } else if ("exclusion".equals(qName)) {
                excs.add(exc);
                exc = null;
            }
        }
    }

    /**
     * Exclusions.
     */
    private final List<MavenDependency> excs

    = new ArrayList<MavenDependency>();

    /**
     * Maven home.
     */
    private static String mavenHome;

    /**
     * Set maven home directory.
     * @param value New value.
     */
    public static void setMavenHome(final String value) {
        mavenHome = value;
    }

    /**
     * @return System.getProperty("user.home")/.m2/repository.
     */
    public static String getMavenHome() {
        if (mavenHome == null) {
            StringWriter sw = new StringWriter();
            sw.append(System.getProperty("user.home"));
            sw.append(Strings.FILE_SEP);
            sw.append(".m2");
            sw.append(Strings.FILE_SEP);
            sw.append("repository");
            mavenHome = sw.toString();
        }
        return mavenHome;
    }

    /**
     * Maven dependency.
     */
    public static final class MavenDependency implements
            Comparable<MavenDependency> {

        /**
         * Is parent set flag.
         */
        private boolean isParentSet = false;

        /**
         * Parent.
         */
        private MavenDependency parent;

        /**
         * @param parentProject
         *            Parent project.
         */
        public void setParent(final MavenDependency parentProject) {
            if (!isParentSet) {
                parent = parentProject;
                isParentSet = true;
            }
        }

        /**
         * @return Parent dependency or empty String if this is top level.
         */
        public MavenDependency getParent() {
            if (parent == null) {
                parent = this;
            }
            return parent;
        }

        /**
         * @param pomFile
         *            pom file.
         */
        public MavenDependency(final File pomFile) {
            pom = pomFile;
        }

        /**
         * @return True only if dependency is required, and not test scope.
         */
        public boolean isRequired() {
            if (isOptional()) {
                return false;
            }
            if ("test".equalsIgnoreCase(getScope())) {
                return false;
            }
            return true;
        }

        /**
         * Pom file this dependency was found in.
         */
        private final File pom;

        /**
         * @return the groupId
         */
        public String getGroupId() {
            if (depGroupId == null) {
                depGroupId = "";
            }
            return depGroupId;
        }

        /**
         * @param groupId
         *            the groupId to set
         */
        public void setGroupId(final String groupId) {
            this.depGroupId = groupId;
        }

        /**
         * @return the artifactId
         */
        public String getArtifactId() {
            if (depArtifactId == null) {
                depArtifactId = "";
            }
            return depArtifactId;
        }

        /**
         * @param artifactId
         *            the artifactId to set
         */
        public void setArtifactId(final String artifactId) {
            this.depArtifactId = artifactId;
        }

        /**
         * @return the version
         */
        public String getVersion() {
            if (depVersion == null) {
                depVersion = "";
            }
            return depVersion;
        }

        /**
         * @param version
         *            the version to set
         */
        public void setVersion(final String version) {
            this.depVersion = version;
        }

        /**
         * @return the scope
         */
        public String getScope() {
            if (depScope == null) {
                depScope = "";
            }
            return depScope;
        }

        /**
         * @param scope
         *            the scope to set
         */
        public void setScope(final String scope) {
            this.depScope = scope;
        }

        /**
         * @return pom file.
         */
        public File getPom() {
            return pom;
        }

        /**
         * Group id.
         */
        private String depGroupId;

        /**
         * Artifact id.
         */
        private String depArtifactId;

        /**
         * Version.
         */
        private String depVersion;

        /**
         * Optional.
         */
        private String optional;

        /**
         * Scope.
         */
        private String depScope;

        @Override
        public int hashCode() {
            return getGroupId().hashCode() + getArtifactId().hashCode()
                    + getVersion().hashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            if (obj == null) {
                return false;
            }
            if (!(obj instanceof MavenAuditor.MavenDependency)) {
                return false;
            }
            MavenAuditor.MavenDependency o = (MavenAuditor.MavenDependency) obj;
            return this.getId().equals(o.getId());
        }

        /**
         * @return id for comparisons.
         */
        private String getId() {
            return getArtifactId() + ";" + getGroupId() + ";" + getVersion();
        }

        /**
         * If there is a relocation, the relocation is used, otherwise the pom
         * group id is used to determine the path from the repository root.
         *
         * @return Path from repository root.
         */
        public String getPathFromRepositoryRoot() {
            StringWriter sw = new StringWriter();
            sw.append(getGroupId().replace(".", Strings.FILE_SEP));
            return sw.toString();
        }

        /**
         * @return Path to pom in local repository.
         */
        public String getRepository() {
            StringWriter sw = new StringWriter();
            sw.append(getPathFromRepositoryRoot());
            sw.append(Strings.FILE_SEP);
            sw.append(getArtifactId());
            sw.append(Strings.FILE_SEP);
            sw.append(getVersion());
            return sw.toString();
        }

        /**
         * @return True if dependency is marked optional in the pom.
         */
        public boolean isOptional() {
            if (Strings.isPopulated(optional)
                    && "true".equalsIgnoreCase(optional)) {
                return true;
            }
            return false;
        }

        /**
         * @return name.
         */
        public String getName() {
            StringWriter sw = new StringWriter();
            sw.append(getArtifactId());
            sw.append("-");
            sw.append(getVersion());
            return sw.toString();
        }

        /**
         * @return Relative path to pom.
         */
        public String getRelativeRepositoryPom() {
            StringWriter sw = new StringWriter();
            sw.append(getRepository());
            sw.append(Strings.FILE_SEP);
            sw.append(getPomName());
            return sw.toString();
        }

        /**
         * @return Relative path to jar.
         */
        public String getRelativeRepositoryJar() {
            StringWriter sw = new StringWriter();
            sw.append(getRepository());
            sw.append(Strings.FILE_SEP);
            sw.append(getJarName());
            return sw.toString();
        }

        /**
         * @return Path to pom in local repository.
         */
        public String getLocalRepositoryPom() {
            StringWriter sw = new StringWriter();
            sw.append(getMavenHome());
            sw.append(Strings.FILE_SEP);
            sw.append(getRelativeRepositoryPom());
            return sw.toString();
        }

        /**
         * @return Path to jar in local repository.
         */
        public String getLocalRepositoryJar() {
            StringWriter sw = new StringWriter();
            sw.append(getMavenHome());
            sw.append(Strings.FILE_SEP);
            sw.append(getRelativeRepositoryJar());
            return sw.toString();
        }

        /**
         * @return Simple jar name.
         */
        public String getJarName() {
            StringWriter sw = new StringWriter();
            sw.append(getName());
            sw.append(".jar");
            return sw.toString();
        }

        /**
         * @return Simple pom name.
         */
        public String getPomName() {
            StringWriter sw = new StringWriter();
            sw.append(getName());
            sw.append(".pom");
            return sw.toString();
        }

        @Override
        public String toString() {
            StringWriter sw = new StringWriter();
            sw.append(RezIO.getCanonicalPath(pom));
            sw.append(",");
            sw.append(getGroupId());
            sw.append(",");
            sw.append(getArtifactId());
            sw.append(",");
            sw.append(getVersion());
            sw.append(",");
            sw.append(getScope());
            return sw.toString();
        }

        @Override
        public int compareTo(final MavenDependency o) {
            return getId().compareTo(o.getId());
        }
    }
}
